/**
 * Author: Jackin (qin.zhang@uni.lu)
 * Date: 2013
 * (c) 2013 University of Luxembourg - Interdisciplinary Centre for Security Reliability and Trust (SnT)
 * All rights reserved
 * GPLv3 to be found at: http://www.gnu.org/licenses/quick-guide-gplv3.html
 */

package lu.uni.geko.ext.ram;

import java.util.HashMap;
import java.util.Iterator;

import lu.uni.geko.common.GeKoConstants;
import lu.uni.geko.resources.MainResourceLoader;
import lu.uni.geko.util.bridges.EMFBridge;
import lu.uni.geko.util.ecore.FeatureEquivalenceHelper;
import lu.uni.geko.weaver.Advice;
import lu.uni.geko.weaver.AdviceEffectuation;
import lu.uni.geko.weaver.add.MainAdder;
import lu.uni.geko.weaver.merge.Merger;
import lu.uni.geko.weaver.remove.Remover;
import lu.uni.geko.weaver.scope.AdviceAndScopeResolver;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;

/**
 * This class is a wrapped weaver for RAM aspects, which adapts partial functionality of the default GeKo weaver.
 * 
 * @author Jackin
 */
public final class RamWeaver
{
	private HashMap<URI, URI> backup; // backup RAM aspects for final recovery after weaving

	/**
	 * Constructor of the wrapped weaver.
	 */
	public RamWeaver()
	{
		this.backup = new HashMap<URI, URI>(); // Pair <baseURI, wovenURI>
	}

	public final void weave(final URI baseURI)
	{
		/*
		 * If the base RAM aspect is still depending on other RAM aspects, go recursively any leaf aspect which doesn't
		 * contain any instantiation entry. Otherwise, weave current aspect with all its dependent aspects fetched from the
		 * instantiation entries. (Depth-first algorithm)
		 */

		// backup the current base aspect into the woven URI
		URI wovenURI = copyBaseToWovenURI(baseURI);
		this.backup.put(baseURI, wovenURI);

		// load current base aspect and its instantiation list
		ram.Aspect base = (ram.Aspect) MainResourceLoader.getUniqueContentRoot(baseURI, "current base model");
		EList<ram.Instantiation> instantiations = base.getInstantiations();

		if (instantiations != null)
		{ // if the current base RAM aspect is still depending on other aspects, then weave its all depending aspects
			for (int instantiationIndex = 0; instantiationIndex < instantiations.size(); instantiationIndex++)
			{ // All the aspects which the current base aspect are depending can be fetched from the instantiations
				try
				{
					// The dependent is an external aspect which the current base aspect is depending on
					ram.Instantiation instantiation = instantiations.get(instantiationIndex);
					ram.Aspect dependent = (ram.Aspect) instantiation.getExternalAspect();
					URI dependentURI = EcoreUtil.getURI(dependent);

					// check if the dependent aspect is loaded
					if (!RamChecker.checkAspect(dependent))
						throw new RuntimeException("Cannot find the dependent RAM aspect: " + dependent.toString()
								+ ", or the expected RAM aspect doesn't contain structural view!");

					// If the dependent aspect is OK, go recursively to weave it with all its dependent aspects
					weave(dependentURI);

					/*
					 * When finishing weaving the dependent aspect with all its own dependent aspects, weave this dependent aspect
					 * onto the current base aspect.
					 */
					performWeaving(baseURI, dependentURI, instantiationIndex);

				} catch (Exception ex)
				{
					ex.printStackTrace();
				}
			} // end of for
		} // end of if

	}

	private final void performWeaving(URI baseURI, URI dependentURI, int instantiationIndex)
	{
		/*
		 * RAM-specific weaving prerequisite: visibility changing
		 * 
		 * According to RAM design, if the dependent aspect is woven to the base aspect, the visibility of "public"
		 * operations should be changed to "package".
		 */
		changeVisibility(dependentURI);

		/*
		 * Instantiate the dependent aspect.
		 * 
		 * Be careful that RAM aspects have two modes of instantiations: "depends" and "extends". The mode "extends" equals
		 * to the default GeKo mode that property values in the advice would override the corresponding ones in the base
		 * model. The other mode "depends", in opposite, keeps the property values in the base model while weaving the
		 * advice onto it. So the mode "depends" is equivalent to "weave base onto advice" by applying GeKo default weaving
		 * functionality.
		 */
		ram.Aspect base = (ram.Aspect) MainResourceLoader.getUniqueContentRoot(baseURI, "current base model");
		ram.Instantiation instantiation = base.getInstantiations().get(instantiationIndex);
		if (instantiation.getType().getLiteral().equalsIgnoreCase(RamConstants.INSTANTIATION_TYPE_DEPENDS))
			instantiate(instantiation, dependentURI);

		Advice advice = AdviceAndScopeResolver.resolveAvAndInstantiationScopes(dependentURI);
		AdviceEffectuation adviceEffectuation = RamAdviceEffectuationCalculator.calculateAdviceEffectuation(baseURI, instantiationIndex);

		Merger merger = new Merger(advice, adviceEffectuation, baseURI);
		FeatureEquivalenceHelper featureEquivalenceHelper = merger.performMergesAndReturnFeatureEquivalenceHelper();
		MainAdder.performAdditions(advice, adviceEffectuation, featureEquivalenceHelper, baseURI);
		Remover.performRemovals(adviceEffectuation);

		// Save the woven result into the baseURI. The original base aspect can be recovered after all weaving tasks done,
		// by calling the "recover" method.
		MainResourceLoader.save(baseURI);
	}

	private final void instantiate(ram.Instantiation instantiation, URI dependentURI)
	{
		ram.Aspect dependent = (ram.Aspect) MainResourceLoader.getUniqueContentRoot(dependentURI, "current dependent model");
		
		/*
		 * Note that the to-element is located in the base model, and the from-element is in the dependent model.
		 * 
		 * Be careful that when instantiate the dependent aspect under "depends" mode, not only the name but also other
		 * attributes should be overridden by the corresponding ones in the base aspect.
		 */
		for (ram.ClassifierMapping classifierMapping : instantiation.getMappings())
		{
			// class mapping
			ram.Class baseClass = (ram.Class) classifierMapping.getToElement();
			ram.Class avClass = null;
			for (ram.Classifier iClass : dependent.getStructuralView().getClasses())
			{
				if (iClass.getName().equalsIgnoreCase(classifierMapping.getFromElement().getName()))
				{
					avClass = (ram.Class) iClass;
					break;
				}
			}

			avClass.setName(baseClass.getName());

			if (baseClass.isAbstract())
				avClass.setAbstract(true);
			else
				avClass.setAbstract(false);

			if (baseClass.isPartial())
				avClass.setPartial(true);
			else
				avClass.setPartial(false);

			// attribute mapping
			if (classifierMapping.getAttributeMappings() != null)
				for (ram.AttributeMapping attributeMapping : classifierMapping.getAttributeMappings())
				{
					ram.Attribute baseAttribute = attributeMapping.getToElement();
					ram.Attribute avAttribute = null;
					for (ram.Attribute iAttr : avClass.getAttributes())
					{
						if (iAttr.getName().equalsIgnoreCase(attributeMapping.getFromElement().getName()))
						{
							avAttribute = iAttr;
							break;
						}
					}

					avAttribute.setName(baseAttribute.getName());

					if (baseAttribute.isStatic())
						avAttribute.setStatic(true);
					else
						avAttribute.setStatic(false);

					avAttribute.setType(baseAttribute.getType());
				}

			// operation mapping
			if (classifierMapping.getOperationMappings() != null)
			{
				for (ram.OperationMapping operationMapping : classifierMapping.getOperationMappings())
				{
					ram.Operation baseOperation = operationMapping.getToElement();
					ram.Operation avOperation = null;
					for (ram.Operation iOp : avClass.getOperations())
					{
						if (iOp.getName().equalsIgnoreCase(operationMapping.getFromElement().getName()))
						{
							avOperation = iOp;
							break;
						}
					}

					avOperation.setName(baseOperation.getName());
					avOperation.setReturnType(baseOperation.getReturnType());
					avOperation.setVisibility(baseOperation.getVisibility());

					if (baseOperation.isAbstract())
						avOperation.setAbstract(true);
					else
						avOperation.setAbstract(false);

					if (baseOperation.isPartial())
						avOperation.setPartial(true);
					else
						avOperation.setPartial(false);

					if (baseOperation.isStatic())
						avOperation.setStatic(true);
					else
						avOperation.setStatic(false);

					// parameter mapping
					if (operationMapping.getParameterMappings() != null)
						for (ram.ParameterMapping parameterMapping : operationMapping.getParameterMappings())
						{
							ram.Parameter baseParameter = parameterMapping.getToElement();
							ram.Parameter avParameter = null;
							for (ram.Parameter iPar : avOperation.getParameters())
							{
								if (iPar.getName().equalsIgnoreCase(parameterMapping.getFromElement().getName()))
								{
									avParameter = iPar;
									break;
								}
							}

							avParameter.setName(baseParameter.getName());
							avParameter.setType(baseParameter.getType());
						}
				}
			}
		} // end of for(ClassifierMapping)

		// save the changes in the dependent aspect
		MainResourceLoader.saveEObjectAsOnlyContent(dependent, dependentURI);
	}

	private final void changeVisibility(URI dependentURI)
	{
		ram.Aspect dependent = (ram.Aspect) MainResourceLoader.getUniqueContentRoot(dependentURI, "current dependent model");

		for (ram.Classifier iClass : dependent.getStructuralView().getClasses())
		{
			for (ram.Operation iOp : ((ram.Class) iClass).getOperations())
				if (iOp.getVisibility().getValue() == ram.Visibility.PUBLIC_VALUE)
					iOp.setVisibility(ram.Visibility.PACKAGE);
		}

		MainResourceLoader.save(dependentURI);
	}

	private final URI copyBaseToWovenURI(URI baseURI)
	{
		EObject baseRootElement = MainResourceLoader.getUniqueContentRoot(baseURI, "base model");
		EObject wovenRootElement = EcoreUtil.copy(baseRootElement);
		URI wovenURI = EMFBridge.newURIWithStringAppendedToFilename(baseURI, GeKoConstants.getWovenMFilenameAppendage());
		MainResourceLoader.saveEObjectAsOnlyContent(wovenRootElement, wovenURI);
		return wovenURI;
	}

	public final void recover()
	{
		Iterator<URI> iter = this.backup.keySet().iterator();

		while (iter.hasNext())
		{
			URI baseURI = iter.next();
			URI wovenURI = this.backup.get(baseURI);

			// switch the contents in baseURI and wovenURI
			ram.Aspect base = (ram.Aspect) MainResourceLoader.getUniqueContentRoot(wovenURI, "the base model for recovery");
			ram.Aspect woven = (ram.Aspect) MainResourceLoader.getUniqueContentRoot(baseURI, "the woven model for recovery");
			MainResourceLoader.saveEObjectAsOnlyContent(base, baseURI);
			MainResourceLoader.saveEObjectAsOnlyContent(woven, wovenURI);
		}
	}
}
